package a5.grammar.abs;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import a5.grammar.interfaces.Grammar;
import a5.grammar.interfaces.Production;

/**
 * <pre>
 * A {@link a5.grammar.interfaces.Grammar Grammar} implementation which uses reflection to dynamically 
 * create and bind productions. This class lazy loads productions as they are required by the Grammar 
 * and maintains a single instance of each required {@link a5.grammar.interfaces.Production Production}.
 * 
 * A Production is said to be "Required by the Grammar" if there is a logic path to the production from 
 * the start production. For example in the following Grammar with 'Program' being the start production, 
 * 'Expr', 'FunctionName', and 'Local' are "Required by the Grammar" while 'Binding' is not.
 * 
 * Program -> Expr | Local
 * Expr -> FunctionName | "IntegerLiteral" 
 * Local -> "with"
 * Binding -> Local Expr
 * 
 * To make this determination, on initialization of a production the class of the loaded production 
 * is analyzed via reflection to determine if any instance fields extend the Production interface. 
 * If so, they are initialized recursively. 
 * </pre> 
 *  
 * @author Jason Campos
 * @version Assignment 5, CS 152 (Programming Paradigms) Fall 2012, Jeffrey Smith
 */
public abstract class GrammarImpl implements Grammar{
	Map<Class<? extends Production>, Production> productions = Collections.synchronizedMap(new HashMap<Class<? extends Production>, Production>());
	
	/**
	 * Loads the requested production and returns an instance of the class.
	 * 
	 * @throws IllegalArgumentExceptiom
	 * Thrown when the requested production does not belong to this grammar. The 
	 * {@link a5.grammar.annotations.Grammar Grammar} annotation is used to specify to which 
	 * grammars the production belongs. 
	 */
	@SuppressWarnings("unchecked")
	public final  <E extends Production> E getProduction(Class<E> production){
		// If this class has already been loaded, just return it.
		if(productions.containsKey(production))
			return (E) productions.get(production);
		
		// If the production does not yet exist, create an instance.
		// Synchronize on the production class to ensure that only one instance is created
		synchronized(production){
			// Make sure the argument class contains the grammar annotation
			if (!production.isAnnotationPresent(a5.grammar.annotations.Grammar.class))
				throw new IllegalArgumentException("The argument class is not part of a grammar.");
			
			// If it does contain the annotation, make sure its part of the current grammar
			a5.grammar.annotations.Grammar grammar = production.getAnnotation(a5.grammar.annotations.Grammar.class);
			boolean isInGrammar = false;
			for(Class<?> clazz : grammar.clazz())
				if (clazz.equals(this.getClass()))
					isInGrammar = true;
			
			if(!isInGrammar)
				throw new IllegalArgumentException("The argument class is not a part of this grammar.");
			
			try {
				// Load the no argument constructor and explicitly set its accessibility 
				// in case it is private. Create a new instance and then add it to the productions map.
				// Finally, initialize any field dependencies for the class.
				Constructor<E> c = production.getDeclaredConstructor();
				c.setAccessible(true);
				E instance = c.newInstance();
				productions.put(production, instance);
				initializeFields(instance);
			} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) {
				e.printStackTrace();
				System.exit(0);
				throw new RuntimeException("Could not create a new instance of class " + production);
			}
		}
		
		// Safe to cast since instance initialization is controlled by this method
		return (E) productions.get(production);
	}
	
	/**
	 * Initializes any {@link a5.grammar.interfaces.Production Production} dependencies 
	 * for the argument instance. 
	 * 
	 * @param instance
	 * The object being initialized.
	 * 
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	@SuppressWarnings("unchecked")
	private final void initializeFields(Object instance) throws IllegalArgumentException, IllegalAccessException{
		for(Field field : instance.getClass().getDeclaredFields()){
			// Load all interfaces for this field and then check if the interface is a Production
			// If so, initialize the field using getProduction() recursively.
			Class<?>[] clazzes = field.getType().getInterfaces();
			for(Class<?> clazz : clazzes){
				if(Production.class.equals(clazz)){
					field.setAccessible(true); // In case the field is private
					field.set(instance, getProduction((Class<? extends Production>)field.getType()));
					break;
				}
			}
		}		
	}
}
